using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using housingsafety.server.Helpers;
using housingsafety.server.Security;
using housingsafety.server.Svcs;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;

namespace housingsafety.server.Hawk
{
    /// <summary>
    ///   Creates a wrapper around an inner <see cref="JwtBearerEvents" /> that fetches
    ///   the API consumer's bearer token out of the query string if it is present.
    ///   If the query string parameter is not present, this wrapper does nothing.
    /// </summary>
    /// <remarks>
    ///   <para>
    ///     This can be used when the application consuming a .NET Core WebAPI needs to
    ///     enable browser sessioins to download images and/or other binary files directly.
    ///   </para>
    ///   <para>
    ///     If a token is specified in both as a query string parameter and in the
    ///     'Authorization' header, the query string parameter "wins" and will be used
    ///     even if it is invalid and the header's token is valid.
    ///   </para>
    /// </remarks>
    public class QueryStringJwtBearerEventsWrapper : JwtBearerEventsWrapperBase
    {
        private readonly List<string> _additionalProperties = new List<string>();
        private readonly IOptions<HawkAuthenticationOptions> _hawkOptions;

        /// <summary>
        ///   This is the actual query string parameter that will be sought in all
        ///   requests to perform authentication. If found, its value is used for
        ///   processing bearer authentication for this web request.
        /// </summary>
        public string QueryStringParameterName { get; }

        /// <summary>
        ///   Wraps an instance of <see cref="JwtBearerEvents" /> with a behavior
        ///   checks for a token in the query string with a name specified in the
        ///   <paramref name="hawkOptions" /> parameter.
        /// </summary>
        /// <param name="inner">
        ///   A base <see cref="JwtBearerEvents" /> implementation that will gain
        ///   this additional query string inspection behavior.
        /// </param>
        /// <param name="hawkOptions"></param>
        ///   The name of the query string parameter that will be sought from requests
        ///   in order to extract a token.
        /// <exception cref="ArgumentNullException">
        ///   Thrown when <paramref name="inner" /> or
        ///   <paramref name="hawkOptions" /> is null.
        /// </exception>
        public QueryStringJwtBearerEventsWrapper(JwtBearerEvents inner
            , IOptions<HawkAuthenticationOptions> hawkOptions) :
            base(inner)
        {
            if (hawkOptions.Value.QueryStringParameterName == null)
            {
                throw new ArgumentNullException(nameof(hawkOptions.Value.QueryStringParameterName));
            }
            else if (string.IsNullOrWhiteSpace(hawkOptions.Value.QueryStringParameterName))
            {
                throw new ArgumentException(
                    $@"The '{nameof(hawkOptions.Value.QueryStringParameterName)}' cannot be null or whitespace.",
                    nameof(hawkOptions.Value.QueryStringParameterName)
                );
            }

            this._hawkOptions = hawkOptions;
            this.QueryStringParameterName = hawkOptions.Value.QueryStringParameterName;
        }

        /// <summary>
        ///   Checks the web request for the <see cref="QueryStringParameterName" /> in
        ///   the request's query string. If it is found, it fetches the token
        ///   and sets it on the provided <see cref="MessageReceivedContext" />.
        /// </summary>
        /// <param name="context">
        ///   The <see cref="MessageReceivedContext" /> that provides details
        ///   on the web request that was made. The query string, among other
        ///   properties relevant to the request, is stored on this value object.
        /// </param>
        /// <exception cref="ArgumentNullException">
        ///   Thrown when <paramref name="context" /> is null.
        /// </exception>
        public override Task MessageReceived(MessageReceivedContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            StringValues values;

            if (!context.Request.Query.TryGetValue(this.QueryStringParameterName, out values))
            {
                return base.MessageReceived(context);
            }

            if (values.Count > 1)
            {
                context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
                context.Fail(
                    $"Only one '{this.QueryStringParameterName}' query string parameter can " +
                    $"be defined. However, {values.Count:N0} were included in the request."
                );

                return base.MessageReceived(context);
            }

            var token = values.Single();

            if (string.IsNullOrWhiteSpace(token))
            {
                context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
                context.Fail(
                    $"The '{this.QueryStringParameterName}' query string parameter was " +
                    $"defined, but a value to represent the token was not included."
                );

                return base.MessageReceived(context);
            }

            //
            context.Token = HandleAuthenticate(context, token).Data;

            return base.MessageReceived(context);
        }

        private RespRet<string> HandleAuthenticate(MessageReceivedContext context, string hawk)
        {
            if (string.IsNullOrEmpty(hawk))
            {
                //Not Basic authentication header
                return new RespRet<string>(false, "HAWK parameter is null");
            }

            if (null == context?.HttpContext?.RequestServices)
            {
                return new RespRet<string>(false, "context is null");
            }

            var contextRequestServices = context.HttpContext.RequestServices;
            var keyProvider = contextRequestServices.GetService<IHawkCredentialProvider>();
            var tokenHelper = contextRequestServices.GetService<ITokenHelper>();

            if (null == keyProvider || null == tokenHelper)
            {
                //Not Basic authentication header
                return new RespRet<string>(false, "tokenHelper is null");
            }

            WebAPISign webApiSign = new WebAPISign();
            if (!webApiSign.FromMsgAuthCode(hawk))
            {
                return new RespRet<string>(false, "HAWK parameter is invalid");
            }

            var credential = keyProvider.GetKeyByKeyIdAsync(webApiSign.UserId, webApiSign.JwtHash)?.Result;
            if (null == credential)
            {
                return new RespRet<string>(false, "Bad credential");
            }

            RespRet respRet = tokenHelper.Valid(webApiSign, credential.Key, context.HttpContext, _hawkOptions.Value.TimestampSkewSec);
            if (!respRet.Succ)
            {
                return new RespRet<string>(false, "Bad mac");
            }

            return new RespRet<string>(true, "Ok", credential.Key);
        }
    }
}